import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { ScriptDAO } from "@App/app/repo/scripts";
import type { SCMetadata, Subscribe, SubscribeScript } from "@App/app/repo/subscribe";
import { SUBSCRIBE_STATUS_DISABLE, SUBSCRIBE_STATUS_ENABLE, SubscribeDAO } from "@App/app/repo/subscribe";
import { type SystemConfig } from "@App/pkg/config/config";
import { type IMessageQueue } from "@Packages/message/message_queue";
import { type Group } from "@Packages/message/server";
import { type ScriptService } from "./script";
import { createScriptInfo, type InstallSource } from "@App/pkg/utils/scriptInstall";
import { type TInstallSubscribe } from "../queue";
import { checkSilenceUpdate, InfoNotification } from "@App/pkg/utils/utils";
import { ltever } from "@App/pkg/utils/semver";
import { fetchScriptBody, parseMetadata, prepareSubscribeByCode } from "@App/pkg/utils/script";
import { cacheInstance } from "@App/app/cache";
import { v4 as uuidv4 } from "uuid";
import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key";
import i18n, { i18nName } from "@App/locales/locales";

export class SubscribeService {
  logger: Logger;
  subscribeDAO = new SubscribeDAO();
  scriptDAO = new ScriptDAO();

  constructor(
    private systemConfig: SystemConfig,
    private group: Group,
    private mq: IMessageQueue,
    private scriptService: ScriptService
  ) {
    this.logger = LoggerCore.logger().with({ service: "subscribe" });
  }

  async install(param: { subscribe: Subscribe }) {
    const logger = this.logger.with({
      subscribeUrl: param.subscribe.url,
      name: param.subscribe.name,
    });
    try {
      await this.subscribeDAO.save(param.subscribe);
      logger.info("upsert subscribe success");
      this.mq.publish<TInstallSubscribe>("installSubscribe", {
        subscribe: param.subscribe,
      });
      return param.subscribe.url;
    } catch (e) {
      logger.error("upsert subscribe error", Logger.E(e));
      throw e;
    }
  }

  async delete(param: { url: string }) {
    const url = param.url;
    const logger = this.logger.with({
      subscribeUrl: url,
    });
    const subscribe = await this.subscribeDAO.get(url);
    if (!subscribe) {
      logger.warn("subscribe not found");
      return false;
    }
    try {
      await Promise.all([
        // 删除相关脚本
        this.scriptDAO
          .find((_, value) => {
            return value.subscribeUrl === url;
          })
          .then((scripts) =>
            Promise.all(
              scripts.map((script) => {
                return this.scriptService.deleteScript(script.uuid);
              })
            )
          ),
        // 删除订阅
        this.subscribeDAO.delete(url),
      ]);
      logger.info("delete subscribe success");
      return true;
    } catch (e) {
      logger.error("uninstall subscribe error", Logger.E(e));
      throw e;
    }
  }

  // 更新订阅的脚本
  async upsertScript(url: string) {
    const subscribe = await this.subscribeDAO.get(url);
    if (!subscribe) return;
    const logger = this.logger.with({
      url: subscribe.url,
      name: subscribe.name,
    });
    // 对比脚本是否有变化
    const addScript: string[] = [];
    const removeScript: SubscribeScript[] = [];
    const scriptUrl = subscribe.metadata.scripturl || [];
    const scripts = Object.keys(subscribe.scripts);
    for (const url of scriptUrl) {
      // 不存在于已安装的脚本中, 则添加
      if (!scripts.includes(url)) {
        addScript.push(url);
      }
    }
    for (const url of scripts) {
      // 不存在于订阅的脚本中, 则删除
      if (!scriptUrl.includes(url)) {
        removeScript.push(subscribe.scripts[url]);
      }
    }

    const notification: string[][] = [[], []];
    const result: Promise<boolean>[] = [];
    // 添加脚本
    addScript.forEach((url) => {
      result.push(
        (async () => {
          const script = await this.scriptService.installByUrl(url, "subscribe", subscribe.url);
          subscribe.scripts[url] = {
            url,
            uuid: script.uuid,
          };
          notification[0].push(i18nName(script));
          return true;
        })().catch((e) => {
          logger.error("install script failed", Logger.E(e));
          return false;
        })
      );
    });
    // 删除脚本
    removeScript.forEach((item) => {
      // 通过uuid查询脚本id
      result.push(
        (async () => {
          const script = await this.scriptDAO.findByUUID(item.uuid);
          if (script) {
            notification[1].push(i18nName(script));
            // 删除脚本
            this.scriptService.deleteScript(script.uuid);
          }
          return true;
        })().catch((e) => {
          logger.error("delete script failed", Logger.E(e));
          return false;
        })
      );
    });

    await Promise.allSettled(result);

    await this.subscribeDAO.update(subscribe.url, subscribe);

    InfoNotification(
      i18n.t("notification.subscribe_update", { subscribeName: subscribe.name }),
      i18n.t("notification.subscribe_update_desc", {
        newScripts: notification[0].join(","),
        deletedScripts: notification[1].join(","),
      })
    );

    logger.info("subscribe update", {
      install: notification[0],
      update: notification[1],
    });

    return true;
  }

  async _checkUpdateAvailable(
    subscribe: {
      url: string;
      name: string;
      checkUpdateUrl?: string;
      metadata: Partial<Record<string, any>>;
    },
    delayFn?: () => Promise<any>
  ): Promise<false | { updateAvailable: true; code: string; metadata: SCMetadata }> {
    const { url, name } = subscribe;
    const logger = this.logger.with({
      url,
      name,
    });
    try {
      if (delayFn) await delayFn();
      const code = await fetchScriptBody(url);
      const metadata = parseMetadata(code);
      if (!metadata) {
        logger.error("parse metadata failed");
        return false;
      }
      const newVersion = metadata.version && metadata.version[0];
      if (!newVersion) {
        logger.error("parse version failed", { version: metadata.version });
        return false;
      }
      let oldVersion = subscribe.metadata.version && subscribe.metadata.version[0];
      if (!oldVersion) {
        oldVersion = "0.0.0";
      }
      // 对比版本大小
      if (ltever(newVersion, oldVersion)) {
        return false;
      }
      return { updateAvailable: true, code, metadata };
    } catch (e) {
      logger.error("check update failed", Logger.E(e));
      return false;
    }
  }

  // 检查更新
  async checkUpdate(url: string, source: InstallSource) {
    const subscribe = await this.subscribeDAO.get(url);
    if (!subscribe) {
      return false;
    }
    await this.subscribeDAO.update(url, { checktime: Date.now() });
    const logger = this.logger.with({
      url: subscribe.url,
      name: subscribe.name,
    });
    const res = await this._checkUpdateAvailable(subscribe);
    if (res) {
      const { code, metadata } = res;
      const { url } = subscribe;
      const uuid = uuidv4();
      try {
        // 进行更新
        if (true === (await this.trySilenceUpdate(code, url))) {
          // slience update
        } else {
          const si = [false, createScriptInfo(uuid, code, url, source, metadata)];
          await cacheInstance.set(`${CACHE_KEY_SCRIPT_INFO}${uuid}`, si);
          chrome.tabs.create({
            url: `/src/install.html?uuid=${uuid}`,
          });
        }
        return true;
      } catch (e) {
        logger.error("check update failed", Logger.E(e));
        return false;
      }
    }
  }

  async trySilenceUpdate(code: string, url: string) {
    const logger = this.logger.with({
      url,
    });
    // 是否静默更新
    const silenceUpdate = await this.systemConfig.getSilenceUpdateScript();
    if (silenceUpdate) {
      try {
        const newSubscribe = await prepareSubscribeByCode(code, url);
        if (checkSilenceUpdate(newSubscribe.oldSubscribe!.metadata, newSubscribe.subscribe.metadata)) {
          logger.info("silence update subscribe");
          this.install({
            subscribe: newSubscribe.subscribe,
          });
          return true;
        }
      } catch (e) {
        logger.error("prepare script failed", Logger.E(e));
      }
    }
  }

  async checkSubscribeUpdate(checkCycle: number, checkDisable: boolean) {
    const list = await this.subscribeDAO.find((_, value) => {
      return value.checktime + checkCycle * 1000 < Date.now();
    });

    for (const subscribe of list) {
      if (!checkDisable && subscribe.status === SUBSCRIBE_STATUS_ENABLE) {
        continue;
      }
      this.checkUpdate(subscribe.url, "system");
    }
  }

  requestCheckUpdate(url: string) {
    return this.checkUpdate(url, "user");
  }

  async enable(param: { url: string; enable: boolean }) {
    const logger = this.logger.with({
      url: param.url,
    });
    try {
      await this.subscribeDAO.update(param.url, {
        status: param.enable ? SUBSCRIBE_STATUS_ENABLE : SUBSCRIBE_STATUS_DISABLE,
      });
      logger.info("enable subscribe success");
      return true;
    } catch (e) {
      logger.error("enable subscribe error", Logger.E(e));
      throw e;
    }
  }

  init() {
    this.group.on("install", this.install.bind(this));
    this.group.on("delete", this.delete.bind(this));
    this.group.on("checkUpdate", this.requestCheckUpdate.bind(this));
    this.group.on("enable", this.enable.bind(this));

    this.mq.subscribe<TInstallSubscribe>("installSubscribe", (message) => {
      this.upsertScript(message.subscribe.url);
    });

    chrome.alarms.clear("checkSubscribeUpdate");
  }
}
